//-------------------------------------------------------------------
//	Copyright (c) 2012-2014 Zhirnov Andrey
//	This file is part of the "UXGEN-ES" project.
//	See copyright notice in "Engine.h".
//-------------------------------------------------------------------

#pragma once

#include "Physics2D/Objects/PhysicsObject.h"

namespace Engine
{
namespace Physics2D
{

	//
	// Physics Trigger
	//

	class Trigger : public PhysicsObject
	{
	// types
	public:
		struct ObjInTrigger
		{
		// types
		public:
			typedef ObjInTrigger	Self;


		// variables
		public:
			Ptr< PhysicsObject >	obj;
			Ptr< b2Fixture >		fixture;
			Ptr< b2Fixture >		selfFixture;


		// methods
		public:
			ObjInTrigger ()
			{}

			ObjInTrigger (PhysicsObject *obj) : obj(obj)
			{}

			ObjInTrigger (PhysicsObject *obj, b2Fixture *fix, b2Fixture *self) :
				obj(obj), fixture(fix), selfFixture(self)
			{}

			bool operator == (const Self &right) const
			{
				return obj == right.obj and fixture == right.fixture and selfFixture == right.selfFixture;
			}

			bool operator != (const Self &right) const
			{
				return not ( *this == right );
			}

			bool operator <  (const Self &right) const
			{
				return	obj != right.obj  ? obj < right.obj  :
						( fixture == right.fixture  ?  selfFixture < right.selfFixture  :  fixture < right.fixture );
			}

			bool operator >  (const Self &right) const
			{
				return	obj != right.obj  ?  obj > right.obj  :
						( fixture == right.fixture  ?  selfFixture > right.selfFixture  :  fixture > right.fixture );
			}

			bool operator >= (const Self &right) const
			{
				return (*this == right) or (*this > right);
			}
		};


		struct ObjInfo
		{
			enum EState
			{
				UNKNOWN = 0,
				ENTERED,
				EXITED,
				MOVED,
			};

			vec2	enterPoint;
			vec2	delta;
			vec2	pos;
			EState	state;
			///
			ObjInfo () : state(UNKNOWN) {}
		};

		typedef map< ObjInTrigger, ObjInfo >		obj_map_t; 

		typedef void (* Callback_t) (Trigger *trigger, const ObjInTrigger &obj, const ObjInfo &info, void *param);


	// variables
	protected:
		Ptr< b2Fixture >	_fixture;
		obj_map_t			_objects;
		Callback_t			_callback;
		void *				_param;
		bool				_withoutFixture;


	// methods
	public:
		Trigger () :
			PhysicsObject( EPhysicsObj::TRIGGER ), _callback( &_DummyCallback ),
			_param(null), _withoutFixture(false)
		{}

		~Trigger ()
		{}


		void CreateBox (b2World *world, const vec2 &size, const vec2 &pos, float angleRad = 0.0f)
		{
			b2PolygonShape shape;
			shape.SetAsBox( size.x, size.y );

			b2FixtureDef fd;
			fd.shape	= &shape;
			fd.density	= 0.0f;
			fd.friction	= 0.0f;
			fd.userData	= this;
			
			b2BodyDef bd;
			bd.type		= b2_dynamicBody;
			bd.userData	= this;
			bd.position	= Utils::Vec2( pos );
			bd.angle	= angleRad;
			
			_body = world->CreateBody( &bd );
			_body->CreateFixture( &fd );
		}


		void CreateCircle (b2World *world, float radius, const vec2 &pos)
		{
			b2CircleShape shape;
			shape.m_radius	= radius;
			
			b2FixtureDef fd;
			fd.shape	= &shape;
			fd.density	= 0.0f;
			fd.friction	= 0.0f;
			fd.userData	= this;
			
			b2BodyDef bd;
			bd.type		= b2_dynamicBody;
			bd.userData	= this;
			bd.position	= Utils::Vec2( pos );
			
			_body = world->CreateBody( &bd );
			_body->CreateFixture( &fd );
		}


		void SetCallback (Callback_t callback, void *param)
		{
			if ( callback != null )
			{
				_callback	= callback;
				_param		= param;
			}
			else
			{
				_callback	= &_DummyCallback;
				_param		= null;
			}
		}


		void OnBeginContact (b2Fixture *selfFixture, PhysicsObject *obj, b2Fixture *fixture)
		{
			usize			idx = -1;
			ObjInTrigger	o = _withoutFixture ? ObjInTrigger( obj ) : ObjInTrigger( obj, fixture, selfFixture );

			if ( _objects.FindIndex( o, idx ) )
			{
				ObjInfo	&	info = _objects[idx].second;

				info.delta	= info.pos - obj->GetPosition();
				info.pos	= obj->GetPosition();
				info.state	= ObjInfo::MOVED;
				
				_callback( this, o, info, _param );
			}
			else
			{
				ObjInfo	info;

				info.pos	= info.enterPoint	= obj->GetPosition();
				info.state	= ObjInfo::ENTERED;
				
				_callback( this, o, info, _param );
			}
		}

		
		void OnEndContact (b2Fixture *selfFixture, PhysicsObject *obj, b2Fixture *fixture)
		{
			usize			idx = -1;
			ObjInTrigger	o = _withoutFixture ? ObjInTrigger( obj ) : ObjInTrigger( obj, fixture, selfFixture );
			
			if ( _objects.FindIndex( o, idx ) )
			{
				ObjInfo		info = _objects[idx].second;

				info.delta	= info.pos - obj->GetPosition();
				info.pos	= obj->GetPosition();
				info.state	= ObjInfo::EXITED;
				
				_callback( this, o, info, _param );

				_objects.EraseFromIndex( idx );
			}
		}


	private:
		static void _DummyCallback (Trigger *trigger, const ObjInTrigger &obj, const ObjInfo &info, void *param) {}
	};




	//
	// Trigger Listener
	//

	class TriggerListener : public b2ContactListener
	{
	// methods
	public:
		TriggerListener ()		{}
		~TriggerListener ()		{}

		
		// b2ContactListener //
		virtual void BeginContact (b2Contact* contact) override
		{
			typedef PhysicsObject::EPhysicsObj	EPhysicsObj;

			b2Fixture *				fixture_A	= contact->GetFixtureA();
			b2Fixture *				fixture_B	= contact->GetFixtureB();

			Ptr< PhysicsObject >	obj_A		= (PhysicsObject *) fixture_A->GetBody()->GetUserData();
			Ptr< PhysicsObject >	obj_B		= (PhysicsObject *) fixture_B->GetBody()->GetUserData();

			if ( obj_A->GetType() != EPhysicsObj::TRIGGER and
				 obj_B->GetType() != EPhysicsObj::TRIGGER )
			{
				return;
			}

			if ( obj_A->GetType() == EPhysicsObj::TRIGGER )
			{
				obj_A.ToPtr< Trigger >()->OnBeginContact( fixture_A, obj_B, fixture_B );
			}

			if ( obj_B->GetType() == EPhysicsObj::TRIGGER )
			{
				obj_B.ToPtr< Trigger >()->OnBeginContact( fixture_B, obj_A, fixture_A );
			}
		}


		virtual void EndContact (b2Contact* contact) override
		{
			typedef PhysicsObject::EPhysicsObj	EPhysicsObj;

			b2Fixture *				fixture_A	= contact->GetFixtureA();
			b2Fixture *				fixture_B	= contact->GetFixtureB();

			int						index_A		= contact->GetChildIndexA();
			int						index_B		= contact->GetChildIndexB();

			Ptr< PhysicsObject >	obj_A		= (PhysicsObject *) fixture_A->GetBody()->GetUserData();
			Ptr< PhysicsObject >	obj_B		= (PhysicsObject *) fixture_B->GetBody()->GetUserData();

			if ( obj_A->GetType() != EPhysicsObj::TRIGGER and
				 obj_B->GetType() != EPhysicsObj::TRIGGER )
			{
				return;
			}

			if ( obj_A->GetType() == EPhysicsObj::TRIGGER )
			{
				obj_A.ToPtr< Trigger >()->OnEndContact( fixture_A, obj_B, fixture_B );
			}

			if ( obj_B->GetType() == EPhysicsObj::TRIGGER )
			{
				obj_B.ToPtr< Trigger >()->OnEndContact( fixture_B, obj_A, fixture_A );
			}
		}


		virtual void PreSolve (b2Contact* contact, const b2Manifold* oldManifold) override
		{
		}


		virtual void PostSolve (b2Contact* contact, const b2ContactImpulse* impulse) override
		{
		}
	};


}	// Physics2D
}	// Engine